/**
 * 
 */
package com.maohi.software.arii;

import java.io.File;
import java.io.InputStream;
import java.net.MalformedURLException;
import java.net.URL;
import java.net.URLClassLoader;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;

import javax.xml.bind.JAXBContext;
import javax.xml.bind.JAXBException;
import javax.xml.bind.Unmarshaller;

import org.apache.log4j.Logger;
import org.apache.log4j.PropertyConfigurator;

import com.google.inject.AbstractModule;
import com.google.inject.Guice;
import com.google.inject.Injector;
import com.google.inject.Key;
import com.google.inject.Module;
import com.google.inject.Singleton;
import com.google.inject.name.Names;
import com.maohi.software.arii.enumerations.AriiParameter;
import com.maohi.software.arii.events.DependencyEvent;
import com.maohi.software.arii.events.DependencyListener;
import com.maohi.software.arii.events.PluginClassLoaderEvent;
import com.maohi.software.arii.events.PluginClassLoaderListener;
import com.maohi.software.arii.exceptions.ActivatorException;
import com.maohi.software.arii.exceptions.AriiException;
import com.maohi.software.arii.exceptions.DependencyMissingException;
import com.maohi.software.arii.modules.ExtensionModule;
import com.maohi.software.arii.modules.ExtensionPointModule;
import com.maohi.software.arii.modules.JaxBModule;
import com.maohi.software.arii.utils.FileFilters;
import com.maohi.software.arii.utils.Keys;
import com.maohi.software.arii.utils.Modules;
import com.maohi.software.arii.utils.Providers;
import com.maohi.software.arii.utils.TypeLiterals;

/**
 * This class start the main framework
 * 
 * @author heifara
 * 
 */
public class Arii implements DependencyListener {

	/**
	 * The main module. Configure the entire DependencyInjection Guice API
	 * 
	 * @author heifara
	 * 
	 */
	protected static class AriiModule extends AbstractModule {
		private static Logger logger = Logger.getLogger(Arii.AriiModule.class.getName());
		private static AriiModule singleton;

		/**
		 * Return the unique instance of {@link AriiModule}
		 * 
		 * @return the unique instance of {@link AriiModule}
		 */
		public static AriiModule getInstance() {
			if (singleton == null) {
				singleton = new AriiModule();
				singleton.configureAll();
			}
			return singleton;
		}

		/*
		 * Guice.createInjector() takes your Modules, and returns a new Injector instance. Most applications will call this method exactly once, in
		 * their main() method.
		 */
		private Injector injector;

		/**
		 * Visibility is private, needed by the singleton design pattern
		 */
		private AriiModule() {
		}

		@Override
		protected void configure() {
			long iStart = System.currentTimeMillis();

			this.bind(ClassLoader.class).to(PluginClassLoader.class).in(Singleton.class);
			this.bind(TypeLiterals.getMapStringPlugin()).toProvider(Providers.getMapStringPlugin());
			this.bind(TypeLiterals.getListDependencyListener()).toProvider(Providers.getListDependencyListener());
			this.bind(TypeLiterals.getListActivator()).toProvider(Providers.getListActivator());
			this.bind(TypeLiterals.getListModule()).toProvider(Providers.getListModule(this, false));
			this.bind(TypeLiterals.getListModule()).annotatedWith(Names.named("override")).toProvider(Providers.getListModule(this, true));
			this.bind(TypeLiterals.getMapModuleModule()).toProvider(Providers.getMapModuleModule());
			this.bind(TypeLiterals.getMapXsdListXml()).toProvider(Providers.getMapXsdListXml());

			logger.debug(String.format("time: %d", System.currentTimeMillis() - iStart));
		}

		private void configureAll() {
			long iStart = System.currentTimeMillis();

			injector = Guice.createInjector(this);

			configureAll(Arii.getInstance(Keys.getListModule()));
			overrideAll(Arii.getInstance(Keys.getListModule()), Arii.getInstance(Keys.getListModuleOverride()));

			logger.debug(String.format("time: %d", System.currentTimeMillis() - iStart));
		}

		/**
		 * This add all {@link Module} contains in the list into the {@link Injector}
		 * 
		 * @param aModules
		 *            the modules list
		 */
		private void configureAll(List<Module> aModules) {
			long iStart = System.currentTimeMillis();

			injector = Guice.createInjector(Modules.removeClass(aModules, this.getClass()));
			injector = injector.createChildInjector(Modules.removeClass(aModules, ExtensionPointModule.class));
			injector = injector.createChildInjector(Modules.removeClass(aModules, ExtensionModule.class));
			injector = injector.createChildInjector(Modules.removeClass(aModules, JaxBModule.class));
			injector = injector.createChildInjector(aModules);

			logger.debug(String.format("Module[%s] time: %d", aModules, System.currentTimeMillis() - iStart));
		}

		public <T> T getInstance(Class<T> aClass) {
			long iStart = System.currentTimeMillis();

			T iObject = this.injector.getInstance(aClass);

			logger.debug(String.format("Class[%s] Returned[%s] time: %d", aClass, iObject, System.currentTimeMillis() - iStart));
			return iObject;
		}

		private void overrideAll(List<Module> aModules, List<Module> aModuleOverrides) {
			long iStart = System.currentTimeMillis();

			injector = Guice.createInjector(com.google.inject.util.Modules.override(aModules).with(aModuleOverrides));

			logger.debug(String.format("Module[%s] time: %d", aModules, System.currentTimeMillis() - iStart));
		}
	}

	/**
	 * The plugin {@link ClassLoader} used to register each plugin
	 * 
	 * @author heifara
	 */
	protected static class PluginClassLoader extends URLClassLoader {
		private static Logger logger = Logger.getLogger(Arii.PluginClassLoader.class.getName());
		private final List<PluginClassLoaderListener> pluginClassLoaderListeners;

		/**
		 * @throws MalformedURLException
		 */
		public PluginClassLoader() {
			super(new URL[] {}, Thread.currentThread().getContextClassLoader());
			Thread.currentThread().setContextClassLoader(this);

			this.pluginClassLoaderListeners = Collections.synchronizedList(new ArrayList<PluginClassLoaderListener>());
		}

		/**
		 * Add a jar file contain in the directory to the classpath
		 * 
		 * @param aDirectory
		 *            the directory file
		 * @throws MalformedURLException
		 */
		public void addFiles(File aDirectory) throws MalformedURLException {
			long iStart = System.currentTimeMillis();

			for (File iFile : aDirectory.listFiles(FileFilters.withExtension(".jar"))) {
				this.addURL(iFile.toURI().toURL());
				this.fireFileAdded(iFile);
			}

			logger.debug(String.format("Directory[%s] time: %d", aDirectory.getName(), System.currentTimeMillis() - iStart));
		}

		/**
		 * Add a {@link PluginClassLoaderListener}
		 * 
		 * @param aListener
		 *            the listener to add
		 */
		synchronized public void addPluginClassListener(PluginClassLoaderListener aListener) {
			long iStart = System.currentTimeMillis();

			this.pluginClassLoaderListeners.add(aListener);

			logger.debug(String.format("Listener[%s] time: %d", aListener, System.currentTimeMillis() - iStart));
		}

		protected void fireFileAdded(File aFile) {
			long iStart = System.currentTimeMillis();

			for (PluginClassLoaderListener iListener : this.pluginClassLoaderListeners) {
				iListener.fileAdded(new PluginClassLoaderEvent(this, aFile));
			}

			logger.debug(String.format("File[%s] time: %d", aFile, System.currentTimeMillis() - iStart));
		}

		/**
		 * Remove a {@link PluginClassLoaderListener}
		 * 
		 * @param aListener
		 *            the listener to remove
		 */
		synchronized public void removePluginClassListener(PluginClassLoaderListener aListener) {
			long iStart = System.currentTimeMillis();

			this.pluginClassLoaderListeners.remove(aListener);

			logger.debug(String.format("Listener[%s] time: %d", aListener, System.currentTimeMillis() - iStart));
		}

		@Override
		public String toString() {
			StringBuilder builder = new StringBuilder();
			builder.append("PluginClassLoader [getURLs()=");
			builder.append(Arrays.toString(this.getURLs()));
			builder.append(", getParent()=");
			builder.append(this.getParent());
			builder.append("]");
			return builder.toString();
		}

	}

	private static Logger logger = Logger.getLogger(Arii.class.getName());

	private static boolean running;

	public static void exit() {
		running = false;
	}

	/**
	 * Return the appropriate instance for <b>aClass</b>
	 * 
	 * @param aClass
	 *            the class
	 * @return the instantiated class
	 */
	public static <T> T getInstance(Class<T> aClass) {
		long iStart = System.currentTimeMillis();

		T iResult = AriiModule.getInstance().injector.getInstance(aClass);

		logger.debug(String.format("Class[%s] Return[%s] time: %d", aClass, iResult, System.currentTimeMillis() - iStart));
		return iResult;
	}

	/**
	 * Return the Object associated to the XMLFile
	 * 
	 * @param aClass
	 *            the class
	 * @param aInputStream
	 *            the input stream
	 * @return the instantiated class
	 * @throws JAXBException
	 */
	@SuppressWarnings("unchecked")
	public static <T> T getInstance(Class<T> aClass, InputStream aInputStream) throws JAXBException {
		long iStart = System.currentTimeMillis();

		JAXBContext iJaxbContext = JAXBContext.newInstance(aClass);

		Unmarshaller iJaxbUnmarshaller = iJaxbContext.createUnmarshaller();
		T iResult = (T) iJaxbUnmarshaller.unmarshal(aInputStream);

		logger.debug(String.format("Class[%s] InputStream[%s] Return[%s] time: %d", aClass, aInputStream, iResult, System.currentTimeMillis() - iStart));
		return iResult;
	}

	/**
	 * Return the appropriate instance for <b>aClass</b> and <b>aId</b>
	 * 
	 * @param aClass
	 *            the class
	 * @param aId
	 *            the id
	 * @return the appropriate instance
	 */
	public static <T> T getInstance(Class<T> aClass, String aId) {
		return AriiModule.getInstance().injector.getInstance(Key.get(aClass, Names.named(aId)));
	}

	/**
	 * @param aKey
	 * @return
	 */
	public static <T> T getInstance(Key<T> aKey) {
		long iStart = System.currentTimeMillis();

		T iResult = AriiModule.getInstance().injector.getInstance(aKey);

		logger.debug(String.format("Key[%s] Return[%s] time: %d", aKey, iResult, System.currentTimeMillis() - iStart));
		return iResult;
	}

	/**
	 * Return the appropriate instance of <b>aClass</b>
	 * 
	 * @param aClass
	 *            the class canonical path
	 * @param aClassLoader
	 *            the classloader
	 * @return the appropriate instance
	 * @throws ClassNotFoundException
	 */
	public static Object getInstance(String aClass, ClassLoader aClassLoader) throws ClassNotFoundException {
		long iStart = System.currentTimeMillis();

		Class<?> iClass = Class.forName(aClass, true, aClassLoader);
		Object iResult = AriiModule.getInstance().injector.getInstance(iClass);

		logger.debug(String.format("Key[%s] Return[%s] time: %d", aClass, iResult, System.currentTimeMillis() - iStart));
		return iResult;
	}

	/**
	 * Main
	 * 
	 * @param args
	 */
	public static void main(String[] args) {
		try {
			Arii iArii = new Arii();
			iArii.start();
		} catch (MalformedURLException aException) {
			logger.error(aException.getMessage(), aException);
		} catch (AriiException aException) {
			logger.error(aException.getMessage(), aException);
		} catch (InterruptedException aException) {
			logger.error(aException.getMessage(), aException);
		} catch (ActivatorException aException) {
			logger.error(aException.getMessage(), aException);
		}
	}

	private PluginClassLoader classLoader;

	private Exception cause;

	protected Arii() {
		// Configure log4j
		PropertyConfigurator.configure(AriiParameter.LOG4J_FILE.value());
		logger.debug(String.format("log4j.property: %s", AriiParameter.LOG4J_FILE.value()));

		// Configure temporary directory
		System.setProperty("java.io.tmpdir", AriiParameter.TMP_DIRECTORY.value());
		logger.debug(String.format("java.io.tmpdir: %s", AriiParameter.TMP_DIRECTORY.value()));

	}

	@Override
	public void dependencyMissing(DependencyEvent aEvent) {
		this.cause = new DependencyMissingException(aEvent.getPlugin(), aEvent.getDependency());
	}

	public void start() throws MalformedURLException, AriiException, InterruptedException, ActivatorException {

		// ClassLoad all jar file
		this.classLoader = (PluginClassLoader) getInstance(ClassLoader.class);
		this.classLoader.addFiles(new File(AriiParameter.PLUGIN_DIRECTORY.value()));

		// Check for dependencies issue
		DependencyHandler iHandler = getInstance(DependencyHandler.class);
		try {
			iHandler.addDependencyListener(this);
			if (!iHandler.checkDependencies(getInstance(Keys.getMapStringPlugin()))) {
				throw new AriiException(this.cause);
			}
		} finally {
			iHandler.removeDependencyListener(this);
		}

		// Load Module
		AriiModule.getInstance().configureAll(getInstance(Keys.getListModule()));
		AriiModule.getInstance().overrideAll(getInstance(Keys.getListModule()), getInstance(Keys.getListModuleOverride()));

		// Execute Activators
		ActivatorHandler iActivatorHandler = getInstance(ActivatorHandler.class);

		iActivatorHandler.initActivators(getInstance(Keys.getListActivator()));
		iActivatorHandler.startActivators(getInstance(Keys.getListActivator()));

		// Wait for method exit() to be executed
		running = true;
		while (running) {
			Thread.sleep(10);
		}

		iActivatorHandler.stopActivators();

	}
}
